﻿// <copyright file="SftFile.cs" company="Sinclair Community College">
// Copyright (c) Sinclair Community College. All rights reserved.
// </copyright>

namespace SinclairCC.AppV
{
    using System;
    using System.Collections.Generic;
    using System.Text;

    /// <summary>
    /// This class represents an SFT file in Microsoft Application Virtualization (App-V).
    /// </summary>
    public class SftFile : IDisposable
    {
        /// <summary>
        /// The differential SFT header (DSFT).
        /// </summary>
        private DifferentialSftHeader dsft;

        /// <summary>
        /// The SFT header (SMFF).
        /// </summary>
        private SmffHeader smff;

        /// <summary>
        /// The properties header (PROP).
        /// </summary>
        private PropertiesHeader prop;

        /// <summary>
        /// The index header (INDX).
        /// </summary>
        private IndexHeader indx;

        /// <summary>
        /// The compressed index header (CIDX).
        /// </summary>
        private CompressedIndexHeader cidx;

        /// <summary>
        /// The block map header (BMAP).
        /// </summary>
        private BlockMapHeader bmap;

        /// <summary>
        /// The content description header (CONT).
        /// </summary>
        private ContentHeader cont;

        /// <summary>
        /// A stream for reading the SFT file.
        /// </summary>
        private System.IO.FileStream inputStream;

        /// <summary>
        /// Reads binary data from the SFT file stream.
        /// </summary>
        private System.IO.BinaryReader reader;

        /// <summary>
        /// The position in the file at which the data sections start.
        /// </summary>
        private long dataSectionStart;

        /// <summary>
        /// Initializes a new instance of the SftFile class.
        /// </summary>
        /// <param name="filePath">The full path of an SFT file.</param>
        public SftFile(string filePath)
        {
            if (System.IO.File.Exists(filePath))
            {
                this.inputStream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read);
                this.reader = new System.IO.BinaryReader(this.inputStream, Encoding.ASCII);

                System.Text.ASCIIEncoding encoding = new ASCIIEncoding();

                while (this.reader.PeekChar() != -1)
                {
                    byte[] bytes = this.reader.ReadBytes(4);

                    string sectionType = encoding.GetString(bytes).ToUpperInvariant();

                    this.reader.BaseStream.Seek(-4, System.IO.SeekOrigin.Current);

                    switch (sectionType)
                    {
                        case "DSFT":
                            this.dsft = new DifferentialSftHeader(this.reader);
                            break;
                        case "SMFF":
                            this.smff = new SmffHeader(this.reader);
                            break;
                        case "PROP":
                            this.prop = new PropertiesHeader(this.reader);
                            break;
                        case "INDX":
                            this.indx = new IndexHeader(this.reader);
                            break;
                        case "CIDX":
                            // TODO: What if prop hasn't been initialized yet?
                            this.cidx = new CompressedIndexHeader(this.reader, this.prop.CompressionType);
                            break;
                        case "BMAP":
                            this.bmap = new BlockMapHeader(this.reader);
                            break;
                        case "CONT":
                            this.cont = new ContentHeader(this.reader);
                            break;
                        case "DATA":
                            // Once a DATA section has been found, we can stop reading the file.
                            this.dataSectionStart = this.reader.BaseStream.Position;
                            this.reader.BaseStream.Seek(0, System.IO.SeekOrigin.End);
                            break;
                        default:
                            // TODO: Do something with unhandled section types.
                            break;
                    }
                }
            }
            else
            {
                throw new System.IO.FileNotFoundException("The specified file could not be found.", filePath);
            }
        }

        /// <summary>
        /// Gets the SFT Header (SMFF).
        /// </summary>
        /// <remarks>
        /// This header is also known as the Softricity Header.
        /// </remarks>
        public SmffHeader Smff
        {
            get { return this.smff; }
        }

        /// <summary>
        /// Gets the Properties Header (PROP).
        /// </summary>
        public PropertiesHeader Properties
        {
            get { return this.prop; }
        }

        /// <summary>
        /// Gets the Index Header (INDX).
        /// </summary>
        public IndexHeader Index
        {
            get { return this.indx; }
        }

        /// <summary>
        /// Gets the Compressed Index Header (CIDX).
        /// </summary>
        public CompressedIndexHeader CompressedIndex
        {
            get { return this.cidx; }
        }

        /// <summary>
        /// Gets the Block Map Header (BMAP).
        /// </summary>
        public BlockMapHeader BlockMap
        {
            get { return this.bmap; }
        }

        /// <summary>
        /// Gets the Context Description Header (CONT).
        /// </summary>
        public ContentHeader ContentDescription
        {
            get { return this.cont; }
        }

        /// <summary>
        /// Retrieves that data block at the specified offset.
        /// </summary>
        /// <param name="offset">
        /// The offset from the beginning of the data section at which the data block is located.
        /// </param>
        /// <returns>
        /// Returns the data block located at the specified offset.
        /// </returns>
        public DataBlock GetDataBlock(int offset)
        {
            this.reader.BaseStream.Seek(this.dataSectionStart, System.IO.SeekOrigin.Begin);
            this.reader.BaseStream.Seek(offset, System.IO.SeekOrigin.Current);
            DataBlock db = new DataBlock(this.reader);
            return db;
        }

        /// <summary>
        /// Exports a DirectoryMapEntry to the given path.
        /// </summary>
        /// <param name="entry">
        /// The Directory Map Entry that is to be exported.
        /// </param>
        /// <param name="path">
        /// The path to which the Directory Map Entry will be exported.
        /// </param>
        public void ExportDirectoryEntry(DirectoryMapEntry entry, string path)
        {
            System.IO.FileStream outputStream = new System.IO.FileStream(path, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None);
            System.IO.BinaryWriter writer = new System.IO.BinaryWriter(outputStream);

            SinclairCC.AppV.BlockMapEntry blockMapEntry = this.bmap.GetBlockMapEntry(entry);
            if (blockMapEntry != null)
            {
                for (int i = 0; i < blockMapEntry.Offsets.Length; i++)
                {
                    DataBlock db = this.GetDataBlock(blockMapEntry.Offsets[i]);
                    db.WriteBytesToFile(writer, this.prop.CompressionType);
                }
            }

            writer.Close();
            outputStream.Close();
        }

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.reader.Close();
            this.inputStream.Close();
            this.inputStream.Dispose();
        }

        #endregion
    }
}
